﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UIT.Controls.Data.Parameters;
using System.Collections;
using System.ComponentModel;
using UIT.Utils;
using System.Linq.Expressions;
using System.Reflection;
using System.Linq.Dynamic;

namespace UIT.Controls.Data
{
    public class ObjectDataProvider : IParametersDataProvider
    {
        private class ObjectSelectArgument : SelectArgument
        {
            private SelectArgument arg;
            private ObjectDataProvider dataProvider;
            public bool StartApplied { get; set; }
            public bool MaxApplied { get; set; }
            public bool SortApplied { get; set; }


            public ObjectSelectArgument(SelectArgument arg, ObjectDataProvider dataProvider)
            {
                this.arg = arg;
                if (arg.RequestedOperations.HasFlag(Operations.Sort))
                    Sort(arg.SortExpression);
                if (arg.RequestedOperations.HasFlag(Operations.Page))
                    Page(arg.StartRowIndex, arg.MaximumRows);

                StartRowIndexParameterName = dataProvider.StartRowIndexParameterName;
                MaximumRowsParameterName = dataProvider.MaximumRowsParameterName;
                SortExpressionParameterName = dataProvider.SortExpressionParameterName;
            }
            public string StartRowIndexParameterName { get; set; }
            public string MaximumRowsParameterName { get; set; }
            public string SortExpressionParameterName { get; set; }

            public override int TotalRowCount
            {
                get { return arg.TotalRowCount; }
                set { arg.TotalRowCount = value; }
            }

        }

        public ObjectDataProvider()
        {
            ConstructorParameters = new ParameterCollection(this);
            SelectParameters = new ParameterCollection(this);
            InsertParameters = new ParameterCollection(this);
            UpdateParameters = new ParameterCollection(this);
            DeleteParameters = new ParameterCollection(this);
            StartRowIndexParameterName = "startRowIndex";
            MaximumRowsParameterName = "maximumRows";
            SortExpressionParameterName = "sortExpression";
        }

        public string TypeName { get; set; }
        public string SelectMethod { get; set; }
        public string SelectCountMethod { get; set; }
        public string InsertMethod { get; set; }
        public string UpdateMethod { get; set; }
        public string DeleteMethod { get; set; }
        public ParameterCollection ConstructorParameters { get; private set; }
        public ParameterCollection SelectParameters { get; private set; }
        public ParameterCollection InsertParameters { get; private set; }
        public ParameterCollection UpdateParameters { get; private set; }
        public ParameterCollection DeleteParameters { get; private set; }

        #region IDataProvider Members

        public string Id { get; set; }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (SelectParameters != null)
                SelectParameters.Dispose();
            if (InsertParameters != null)
                InsertParameters.Dispose();
            if (UpdateParameters != null)
                UpdateParameters.Dispose();
            if (DeleteParameters != null)
                DeleteParameters.Dispose();
        }

        #endregion

        #region IDataProvider Members


        public bool CanSelect
        {
            get { return !string.IsNullOrEmpty(SelectMethod); }
        }

        public bool CanInsert
        {
            get { return !string.IsNullOrEmpty(InsertMethod); }
        }

        public bool CanDelete
        {
            get { return !string.IsNullOrEmpty(DeleteMethod); }
        }

        public bool CanUpdate
        {
            get { return !string.IsNullOrEmpty(UpdateMethod); }
        }

        #endregion

        private void RaiseError(Exception ex)
        {
            throw new Exception("", ex);
        }

        public class CompiledObjectProvider
        {
            #region Cache Implementation

            private class CacheKey
            {
                public CacheKey(ObjectDataProvider dataProvider)
                {
                    TypeName = dataProvider.TypeName;
                    ConstructorSignature = '(' + String.Join(",", dataProvider.ConstructorParameters.Select(param => param.Name)) + ')';
                    SelectMethod = dataProvider.SelectMethod;
                    UpdateMethod = dataProvider.UpdateMethod;
                    DeleteMethod = dataProvider.DeleteMethod;
                    InsertMethod = dataProvider.InsertMethod;
                }

                public string TypeName { get; set; }
                public string ConstructorSignature { get; set; }
                public string SelectMethod { get; set; }
                public string UpdateMethod { get; set; }
                public string DeleteMethod { get; set; }
                public string InsertMethod { get; set; }

                public class EqualityComparer : IEqualityComparer<CacheKey>
                {
                    public bool Equals(CacheKey x, CacheKey y)
                    {
                        return x.TypeName == y.TypeName
                            && x.ConstructorSignature == y.ConstructorSignature
                            && x.SelectMethod == y.SelectMethod
                            && x.UpdateMethod == y.UpdateMethod
                            && x.DeleteMethod == y.DeleteMethod
                            && x.InsertMethod == y.InsertMethod;
                    }

                    public int GetHashCode(CacheKey obj)
                    {
                        return obj.GetHashCode();
                    }
                }

            }

            private class CacheValue
            {
                public Type Type { get; set; }
                public ConstructorInfo Constructor { get; set; }
                public MethodInfo SelectMethod { get; set; }
                public MethodInfo SelectCountMethod { get; set; }
                public MethodInfo UpdateMethod { get; set; }
                public MethodInfo DeleteMethod { get; set; }
                public MethodInfo InsertMethod { get; set; }
            }

            private static Dictionary<CacheKey, CacheValue> cache = new Dictionary<CacheKey, CacheValue>(new CacheKey.EqualityComparer());

            public static CompiledObjectProvider Find(ObjectDataProvider dataProvider)
            {
                CacheValue value;
                lock (cache)
                {
                    if (!cache.TryGetValue(new CacheKey(dataProvider), out value))
                        return new CompiledObjectProvider(dataProvider, value);

                    value = new CacheValue();
                    cache.Add(new CacheKey(dataProvider), value);
                }
                value.Type = Type.GetType(dataProvider.TypeName);
                value.Constructor = FindConstructor(value.Type, dataProvider.ConstructorParameters.Select(param => param.Name));
                IEnumerable<string> selectParameters = dataProvider.SelectParameters.Select(param => param.Name);
                if (dataProvider.EnablePaging)
                    selectParameters = selectParameters.Union(new string[] { dataProvider.StartRowIndexParameterName, dataProvider.MaximumRowsParameterName });
                if (dataProvider.EnableSorting)
                    selectParameters = selectParameters.Union(new string[] { dataProvider.SortExpressionParameterName });
                selectParameters = selectParameters.Distinct();
                value.SelectMethod = FindMethod(value.Type, dataProvider.SelectMethod, selectParameters);
                value.SelectCountMethod = FindMethod(value.Type, dataProvider.SelectCountMethod, selectParameters.Except(new string[] { dataProvider.StartRowIndexParameterName, dataProvider.MaximumRowsParameterName, dataProvider.SortExpressionParameterName }));
                value.UpdateMethod = FindMethod(value.Type, dataProvider.UpdateMethod, dataProvider.UpdateParameters.Select(param => param.Name));
                value.DeleteMethod = FindMethod(value.Type, dataProvider.DeleteMethod, dataProvider.DeleteParameters.Select(param => param.Name));
                value.InsertMethod = FindMethod(value.Type, dataProvider.InsertMethod, dataProvider.InsertParameters.Select(param => param.Name));
                return new CompiledObjectProvider(dataProvider, value);
            }

            private static MethodInfo FindMethod(Type type, string methodName, IEnumerable<string> parameterNames)
            {
                return (MethodInfo)FindMethod(type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).Where(m => m.Name == methodName), parameterNames);
            }

            private static MethodBase FindMethod(IEnumerable<MethodBase> methods, IEnumerable<string> parameterNames)
            {
                foreach (ConstructorInfo m in methods)
                {
                    var parametersInfo = m.GetParameters();
                    if (parametersInfo.Select(p => p.Name).Except(parameterNames).Any())
                        return m;
                }
                return null;
            }

            private static ConstructorInfo FindConstructor(Type type, IEnumerable<string> parameterNames)
            {
                return (ConstructorInfo)FindMethod(type.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public), parameterNames);
            }

            #endregion

            private Delegate select;
            private Delegate selectCount;
            private Delegate insert;
            private Delegate update;
            private Delegate delete;
            private ObjectDataProvider dataProvider;

            private CompiledObjectProvider(ObjectDataProvider dataProvider, CacheValue cache)
            {
                var instance = dataProvider.GetTypeInstance(cache.Constructor);
                dataProvider.ObjectCreated(dataProvider, new InstanceEventArgs { Instance = instance });
                select = cache.SelectMethod.ToDelegate(instance);
                selectCount = cache.SelectCountMethod.ToDelegate(instance);
                insert = cache.InsertMethod.ToDelegate(instance);
                update = cache.UpdateMethod.ToDelegate(instance);
                delete = cache.DeleteMethod.ToDelegate(instance);
                this.dataProvider = dataProvider;
            }

            public IEnumerable Select(SelectArgument arg)
            {
                ObjectSelectArgument oArg = new ObjectSelectArgument(arg, dataProvider);
                if (dataProvider.EnablePaging)
                    if (selectCount != null)
                        arg.TotalRowCount = Invoke<int>(selectCount, dataProvider.SelectParameters, oArg);
                var result = Invoke<IEnumerable>(select, dataProvider.SelectParameters, oArg) ?? Enumerable.Empty<object>();
                if (arg.RequestedOperations.HasFlag(Operations.Page) && !oArg.StartApplied && !oArg.MaxApplied || arg.RequestedOperations.HasFlag(Operations.Sort) && !oArg.SortApplied)
                {
                    var queryableResult = result.AsQueryable();
                    if (arg.RequestedOperations.HasFlag(Operations.Page) && !oArg.StartApplied && !oArg.MaxApplied)
                    {
                        if (arg.TotalRowCount == 0)
                            arg.TotalRowCount = queryableResult.Count();
                        queryableResult = queryableResult.Skip(arg.StartRowIndex).Take(arg.MaximumRows);
                    }
                    if (arg.RequestedOperations.HasFlag(Operations.Sort) && !oArg.SortApplied)
                    {
                        queryableResult = queryableResult.OrderBy(arg.SortExpression);
                    }
                    return queryableResult;
                }
                return result;
            }

            private T Invoke<T>(Delegate select, ParameterCollection parameterCollection, ObjectSelectArgument arg = null)
            {
                try
                {
                    return (T)select.DynamicInvoke(BuildParameters(dataProvider.SelectParameters, select.Method, arg));
                }
                catch (Exception ex)
                {
                    dataProvider.RaiseError(ex);
                    return default(T);
                }

            }

            private void Invoke(Delegate select, ParameterCollection parameterCollection)
            {
                try
                {
                    select.DynamicInvoke(BuildParameters(dataProvider.SelectParameters, select.Method));
                }
                catch (Exception ex)
                {
                    dataProvider.RaiseError(ex);
                }
            }

            public static object[] BuildParameters(ParameterCollection parameterCollection, MethodBase method)
            {
                return BuildParameters(parameterCollection, method, null);
            }

            private static object[] BuildParameters(ParameterCollection parameterCollection, MethodBase method, ObjectSelectArgument arg)
            {
                var parametersInfo = method.GetParameters();
                object[] parameters = new object[parametersInfo.Length];
                for (int i = 0; i < parametersInfo.Length; i++)
                {
                    if (arg.RequestedOperations.HasFlag(Operations.Page) && arg != null)
                    {
                        if (parametersInfo[i].Name == arg.StartRowIndexParameterName)
                        {
                            parameters[parametersInfo[i].Position] = arg.StartRowIndex;
                            arg.StartApplied = true;
                            continue;
                        }
                        else if (parametersInfo[i].Name == arg.MaximumRowsParameterName)
                        {
                            parameters[parametersInfo[i].Position] = arg.MaximumRows;
                            arg.MaxApplied = true;
                            continue;
                        }
                    }
                    if (arg.RequestedOperations.HasFlag(Operations.Sort) && arg != null && parametersInfo[i].Name == arg.SortExpressionParameterName)
                    {
                        parameters[parametersInfo[i].Position] = arg.SortExpression;
                        arg.SortApplied = true;
                        continue;
                    }
                    parameters[parametersInfo[i].Position] = parameterCollection.Where((param, j) => param.Name == parametersInfo[i].Name || j == parametersInfo[i].Position).FirstOrDefault().Value;
                    object convertedParameter;
                    if (!parameters[parametersInfo[i].Position].TryConvert(parametersInfo[i], out convertedParameter))
                        throw new InvalidOperationException(string.Format("Could not cast {0} to {1} or could not find a method accepting {0} as {2}th parameter", parameters[parametersInfo[i].Position], parametersInfo[i], parametersInfo[i].Position));

                    parameters[parametersInfo[i].Position] = convertedParameter;
                }
                return parameters;
            }

            public bool Insert()
            {
                Invoke(insert, dataProvider.InsertParameters);
                return true;
            }

            public bool Update()
            {
                Invoke(update, dataProvider.UpdateParameters);
                return true;

            }

            public bool Delete()
            {
                Invoke(delete, dataProvider.DeleteParameters);
                return true;
            }
        }

        public event EventHandler<InstanceEventArgs> ObjectCreating;
        public event EventHandler<InstanceEventArgs> ObjectCreated;

        internal object GetTypeInstance(ConstructorInfo ctor)
        {
            InstanceEventArgs args = new InstanceEventArgs();
            if (ObjectCreating != null)
                ObjectCreating(this, args);

            if (args.Cancel)
                return null;
            return args.Instance ?? ctor.Invoke(CompiledObjectProvider.BuildParameters(ConstructorParameters, ctor));
        }

        public class InstanceEventArgs : CancelEventArgs
        {
            public InstanceEventArgs()
            {

            }

            public object Instance { get; set; }
        }



        public IEnumerable Select(SelectArgument arg)
        {
            if (!CanSelect)
                throw new InvalidOperationException("You cannot get data since you have not provided a SelectMethod");
            return CompiledObjectProvider.Find(this).Select(arg);
        }

        public void Update()
        {
            if (!CanUpdate)
                throw new InvalidOperationException("You cannot update data since you have not provided an UpdateMethod");
            CompiledObjectProvider.Find(this).Update();
        }

        public void Insert()
        {
            if (!CanInsert)
                throw new InvalidOperationException("You cannot insert data since you have not provided a InsertMethod");
            CompiledObjectProvider.Find(this).Insert();
        }

        public void Delete()
        {
            if (!CanDelete)
                throw new InvalidOperationException("You cannot delete data since you have not provided a DeleteMethod");
            CompiledObjectProvider.Find(this).Delete();
        }

        public IControl Parent
        {
            get;
            set;
        }

        #region IDataProvider Members


        public Operations SupportedOperations
        {
            get
            {
                Operations op = Operations.None;
                if (CanSelect)
                    op = op | Operations.Select;
                if (CanUpdate)
                    op = op | Operations.Update;
                if (CanInsert)
                    op = op | Operations.Insert;
                if (CanDelete)
                    op = op | Operations.Delete;
                if (EnablePaging && CanSelect)
                    op = op | Operations.Page;
                if (EnableSorting && CanSelect)
                    op = op | Operations.Sort;
                return op;
            }
        }

        #endregion

        public bool EnablePaging { get; set; }
        public bool EnableSorting { get; set; }
        public string StartRowIndexParameterName { get; set; }
        public string MaximumRowsParameterName { get; set; }
        public string SortExpressionParameterName { get; set; }
    }
}
